feat(auth): TOTP second factor for the local provider - #779
Conversation
|
| GitGuardian id | GitGuardian status | Secret | Commit | Filename | |
|---|---|---|---|---|---|
| 36754989 | Triggered | Generic Password | 7901aaa | tests/api/auth/login.test.ts | View secret |
🛠 Guidelines to remediate hardcoded secrets
- Understand the implications of revoking this secret by investigating where it is used in your code.
- Replace and store your secret safely. Learn here the best practices.
- Revoke and rotate this secret.
- If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.
To avoid such incidents in the future consider
- following these best practices for managing and storing secrets including API keys and other credentials
- install secret detection on pre-commit to catch secret before it leaves your machine and ease remediation.
🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Adds optional TOTP (RFC 6238) on top of the local email/password provider, opt-in per account through ADMIN_TOTP_SECRET and USER_TOTP_SECRET. SSO already covered MFA by delegating to the identity provider (docs/OIDC.md); this closes the gap for deployments that authenticate locally. Verification is in-tree and dependency-free: HOTP is a truncated HMAC and base32 is a 32-character alphabet, so an OTP library would add supply-chain surface to the one code path that exists to raise the cost of a compromise. HMAC-SHA-1 is RFC 6238's default and the only algorithm authenticator apps interoperate on for a bare otpauth URI. Secrets are env vars rather than enrolled state: the chart and the image both run on a read-only filesystem, and a second factor that silently degraded when the data dir was unwritable would be worse than none. A value that is not base32 is an AuthConfigError, so a typo stops login with a 503 naming the variable instead of quietly dropping the factor or rejecting every correct code. Notable properties: - The password alone never creates a session, and an accepted code cannot be replayed inside its 90-second window (RFC 6238 5.2). Both are asserted in tests/security/mfa-second-factor.test.ts, now control 1.6 in docs/SECURITY.md. - Replying "code required" is not the enumeration oracle control 1.5 removes: it is reachable only with a correct password, and without MFA that same request would have returned a session. - Being asked for a code costs no rate-limit budget; a wrong code spends both buckets. login_client allows five failures per five minutes, so charging the prompt would cap legitimate users at five logins per window. - NEXT_PUBLIC_AUTH_PROVIDER=oidc changes what the login page renders, not what POST /api/auth/login accepts. An OIDC deployment that still sets ADMIN_PASSWORD keeps a route that never reaches the issuer; these variables are honoured in that mode too, which is how it is closed. Documented rather than assumed away. - The chart carries the secret in its Secret and references it from the pod, so it never lands in the Deployment spec the way extraEnv would. Both refs stay optional even in strict mode, so an unasked-for factor cannot block startup. Chart version bumped to 0.1.63 per the packaged-file rule (libredb#167); 0.1.62 is already released. Coverage is 100% on all four touched source files. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review findings on the TOTP work, each measured against a running server. The alphabet was validated and the length was not, so ADMIN_TOTP_SECRET=AA was accepted and signed in on an 8-bit key that a single observed code recovers outright. The module already argues that a malformed secret must fail loudly rather than leave a silently degraded factor; that applies with more force to one carrying no entropy, so it is now an AuthConfigError naming the variable, checked on the decoded bytes. The chart pattern and the app's reader disagreed in four cases. Two of them installed and then took the login route down with a 503, which is the exact outcome the chart README promises the schema prevents. The test now derives its expectation from the app's own reader, so neither side can drift alone. docs/MFA.md contradicted itself on whether these variables apply under OIDC. Verified against a live oidc-mode server: they do. The API route stays guarded and only the login page has no password form to hang the field on. Also: the redundant ternary CodeQL reported as a user-controlled bypass, the login-page doc that still described a two-field form, the systemd env template that gave .deb operators no sign MFA exists, and an eviction comment naming a threat that cannot reach the function it guards.
4444029 to
0978773
Compare
|
Good work. The algorithm is validated against all five RFC 6238 Appendix B vectors, the tests killed every mutant I threw at the module, and both chart trees stay in lockstep. Rather than send you round again I pushed the fixes myself. Please review them before this goes in; I am not merging until you have. Your commit was rewritten, so the branch is force-updated. Secret Scan is a required check and gitleaks found 6 hits, all fabricated fixtures. A Three findings worth your time:
Smaller ones: dropped the Verified locally: format, lint, typecheck, knip, all four drift guards, Two things left for you: link #777, and fill in the PR template. |
The example was the canonical secret from the otpauth documentation, so a deployment that uncommented the line and forgot to replace it would read as protected on every screen while anyone could compute its codes. That is worse than having no second factor, because it also passes an audit. Nothing in the code ever defaults to it, and the line is commented, so this is a human-error surface rather than a live hole; it is still the one value that must not be left in place. .env.example now carries a placeholder that is not base32, matching every other secret in that file, so pasting it verbatim earns the 503 this feature already raises instead of quietly installing a published factor. docs/MFA.md and both chart READMEs generate the secret into a shell variable rather than printing one, which is what the real workflow looks like anyway: generate, enrol, deploy. The otpauth URI was the most exposed of the four, since it can be scanned straight into a phone. Also records that the client rate-limit bucket is keyed on the address, not the account. Measured: five wrong codes for the admin, then a correct password for the other account from that same address, answers 429 for the rest of the window. That is exactly the admin-plus-automation split this feature invites, and behind one NAT the two share the budget.
|
One more push, docs only, and this one is a fast-forward so your commit is untouched. The example secret had to go.
Also documented one thing this feature makes reachable that was not before. The Gates re-run green: format, lint, typecheck, knip, all four drift guards, |
|
Putting the local verification on the record before this goes in. All of it ran against a live server with a real authenticator secret, not just the suite. Second factor
Rate limiting
Configuration failures
OIDC mode
No TOTP configured, this branch and
Helm
Suite
I will merge this shortly. |
Adds optional TOTP (RFC 6238) on top of the local email/password provider, opt-in per account through ADMIN_TOTP_SECRET and USER_TOTP_SECRET. SSO already covered MFA by delegating to the identity provider (docs/OIDC.md); this closes the gap for deployments that authenticate locally.
Verification is in-tree and dependency-free: HOTP is a truncated HMAC and base32 is a 32-character alphabet, so an OTP library would add supply-chain surface to the one code path that exists to raise the cost of a compromise. HMAC-SHA-1 is RFC 6238's default and the only algorithm authenticator apps interoperate on for a bare otpauth URI.
Secrets are env vars rather than enrolled state: the chart and the image both run on a read-only filesystem, and a second factor that silently degraded when the data dir was unwritable would be worse than none. A value that is not base32 is an AuthConfigError, so a typo stops login with a 503 naming the variable instead of quietly dropping the factor or rejecting every correct code.
Notable properties:
Chart version bumped to 0.1.63 per the packaged-file rule (#167); 0.1.62 is already released. Coverage is 100% on all four touched source files.
Description
Type of Change
Related Issue
Closes #
Changes Made
Testing
Test Environment
Screenshots (if applicable)
Checklist
bun run test:coverageandbun run coverage:check)src/lib/db/providers/, I updated the matchingdocs/providers/documentation andtests/integration/db/tests in the same PR (provider triad)Additional Notes